/*
 *
 *  * Copyright (c) iwindplus Technologies Co., Ltd.2024-2030, All rights reserved.
 *
 *
 */

package com.iwindplus.boot.web.manager.context;

import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.extra.servlet.JakartaServletUtil;
import com.iwindplus.boot.domain.constant.CommonConstant.HeaderConstant;
import com.iwindplus.boot.domain.constant.CommonConstant.SymbolConstant;
import com.iwindplus.boot.domain.constant.CommonConstant.TraceConstant;
import com.iwindplus.boot.domain.context.HeaderContextHolder;
import com.iwindplus.boot.domain.context.UserContextHolder;
import com.iwindplus.boot.domain.vo.TraceInfoVO;
import com.iwindplus.boot.domain.vo.UserBaseVO;
import com.iwindplus.boot.util.CryptoUtil;
import com.iwindplus.boot.util.HttpsUtil;
import com.iwindplus.boot.util.JacksonUtil;
import com.iwindplus.boot.web.domain.property.FilterProperty;
import com.iwindplus.boot.web.domain.property.FilterProperty.FilterCryptoConfig;
import io.micrometer.tracing.Span;
import io.micrometer.tracing.TraceContext;
import io.micrometer.tracing.Tracer;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.MDC;
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Component;

/**
 * 上下文信息传递管理器.
 *
 * @author zengdegui
 * @since 2025/02/15 21:45
 */
@Slf4j
@Component
public class ContextTransManager {

    @Resource
    private FilterProperty filterProperty;

    @Resource
    private Tracer tracer;

    /**
     * 解析用户信息.
     *
     * @param userInfoStr 请求头用户信息字符串
     * @return UserBaseVO
     */
    public UserBaseVO getUserInfo(String userInfoStr) {
        final FilterCryptoConfig crypto = this.filterProperty.getCrypto();
        final String data = CryptoUtil.decrypt(userInfoStr, crypto);
        return JacksonUtil.parseObject(data, UserBaseVO.class);
    }

    /**
     * 获取跟踪信息.
     *
     * @return TraceInfoVO
     */
    public TraceInfoVO getTraceInfo() {
        // 延迟获取 Span，避免过早调用
        Span currentSpan = tracer.currentSpan();
        if (ObjectUtil.isEmpty(currentSpan)) {
            currentSpan = tracer.nextSpan().start();
        }

        final TraceContext context = currentSpan.context();
        final TraceInfoVO data = TraceInfoVO.builder()
            .traceId(context.traceId())
            .spanId(context.spanId())
            .realIp(MDC.get(TraceConstant.REAL_IP))
            .build();

        // 如果主动创建 Span，需手动结束（或绑定到请求生命周期
        if (currentSpan.isNoop()) {
            currentSpan.end();
        }
        return data;
    }

    /**
     * 构造请求头参数，并存储至MDC（http请求）.
     *
     * @param headers 请求头集合
     * @return Map<String, String>
     */
    public Map<String, String> buildRequestHeaderInMdc(Map<String, String> headers) {
        Map<String, String> map = this.getMapByHeaders(headers);
        // 跟踪信息上下文
        this.buildTraceInfoInMdc(map);
        // 国际化上下文
        this.buildI18nInfoInMdc(map);
        // 用户信息上下文
        this.buildUserInfoInMdc(map);
        // 灰度发布上下文
        this.buildGrayscaleInfoInMdc(map);
        // 请求信息上下文
        HeaderContextHolder.setContext(map);
        return map;
    }

    /**
     * 构造请求头参数，并存储至MDC（http请求）.
     *
     * @param httpServletRequest 请求
     * @return Map<String, String>
     */
    public Map<String, String> buildRequestHeaderContext(HttpServletRequest httpServletRequest) {
        // 请求头信息
        Map<String, String> map = this.getHeaders(httpServletRequest);
        // 跟踪信息上下文
        this.buildTraceContext(map, httpServletRequest);
        // 国际化上下文
        this.buildI18nInfoInMdc(map);
        // 用户信息上下文
        this.buildUserContext(map);
        // 灰度发布上下文
        this.buildGrayscaleInfoInMdc(map);
        // 请求信息上下文
        HeaderContextHolder.setContext(map);
        return map;
    }

    private void buildGrayscaleInfoInMdc(Map<String, String> map) {
        String version = map.get(HeaderConstant.X_REQUESTED_VERSION);
        if (ObjectUtil.isEmpty(version)) {
            return;
        }
        MDC.put(HeaderConstant.X_REQUESTED_VERSION, version);
    }

    private void buildI18nInfoInMdc(Map<String, String> map) {
        String language = map.get(HttpHeaders.ACCEPT_LANGUAGE);
        if (ObjectUtil.isEmpty(language)) {
            final Locale locale = Locale.getDefault();
            language = new StringBuilder(locale.getLanguage())
                .append(SymbolConstant.HORIZONTAL_LINE)
                .append(locale.getCountry()).toString();
        }
        map.put(HttpHeaders.ACCEPT_LANGUAGE, language);
        MDC.put(HttpHeaders.ACCEPT_LANGUAGE, language);
    }

    private void buildTraceInfoInMdc(Map<String, String> map) {
        TraceInfoVO traceInfo = this.buildTraceInfoByMap(map);
        this.buildTraceInfoInMdc(map, traceInfo);
    }

    private void buildTraceContext(Map<String, String> headers, HttpServletRequest httpServletRequest) {
        TraceInfoVO traceInfo = this.buildTraceInfoByMap(headers);
        if (ObjectUtil.isEmpty(traceInfo.getRealIp())) {
            traceInfo.setRealIp(JakartaServletUtil.getClientIP(httpServletRequest));
        }
        this.buildTraceInfoInMdc(headers, traceInfo);
    }

    private TraceInfoVO buildTraceInfoByMap(Map<String, String> map) {
        String traceId = map.get(TraceConstant.TRACE_ID);
        String spanId = map.get(TraceConstant.SPAN_ID);
        String realIp = map.get(TraceConstant.REAL_IP);
        if (ObjectUtil.isEmpty(traceId) || ObjectUtil.isEmpty(spanId)) {
            return this.getTraceInfo();
        }
        return TraceInfoVO.builder()
            .traceId(traceId)
            .spanId(spanId)
            .realIp(realIp)
            .build();
    }

    private void buildTraceInfoInMdc(Map<String, String> map, TraceInfoVO traceInfo) {
        map.put(TraceConstant.TRACE_ID, traceInfo.getTraceId());
        MDC.put(TraceConstant.TRACE_ID, traceInfo.getTraceId());
        map.put(TraceConstant.SPAN_ID, traceInfo.getSpanId());
        MDC.put(TraceConstant.SPAN_ID, traceInfo.getSpanId());
        if (ObjectUtil.isEmpty(traceInfo.getRealIp())) {
            return;
        }
        map.put(TraceConstant.REAL_IP, traceInfo.getRealIp());
        MDC.put(TraceConstant.REAL_IP, traceInfo.getRealIp());
    }

    private void buildUserContext(Map<String, String> headers) {
        String userInfoStr = this.buildUserInfoInMdc(headers);

        UserBaseVO userInfo = this.getUserInfo(userInfoStr);
        UserContextHolder.setContext(userInfo);
    }

    private String buildUserInfoInMdc(Map<String, String> map) {
        final FilterCryptoConfig crypto = this.filterProperty.getCrypto();

        String userInfoStr = map.get(HeaderConstant.USER_INFO);
        if (ObjectUtil.isEmpty(userInfoStr)) {
            UserBaseVO userInfo = UserContextHolder.getDefaultUser();
            final String data = JacksonUtil.toJsonStr(userInfo);
            userInfoStr = CryptoUtil.encrypt(data, crypto);
        }

        map.put(HeaderConstant.USER_INFO, userInfoStr);
        MDC.put(HeaderConstant.USER_INFO, userInfoStr);
        return userInfoStr;
    }

    private Map<String, String> getMapByHeaders(Map<String, String> headers) {
        Map<String, String> result = new HashMap<>(16);
        Map<String, String> mdcMap = MDC.getCopyOfContextMap();
        this.filterMap(result, mdcMap);
        this.filterMap(result, headers);
        return result;
    }

    private void filterMap(Map<String, String> result, Map<String, String> map) {
        if (MapUtil.isEmpty(map)) {
            return;
        }
        final Map<String, String> tmpMap = map.entrySet().stream()
            .filter(entry -> ObjectUtil.isNotEmpty(entry.getValue()))
            .collect(Collectors.toMap(Entry::getKey, Entry::getValue));
        if (MapUtil.isEmpty(tmpMap)) {
            return;
        }
        result.putAll(tmpMap);
    }

    private Map<String, String> getHeaders(HttpServletRequest httpServletRequest) {
        Map<String, String> headers = HttpsUtil.getHeaders(httpServletRequest);
        if (MapUtil.isEmpty(headers)) {
            return new HashMap<>(16);
        }
        headers.remove(HttpHeaders.AUTHORIZATION);
        headers.remove(HeaderConstant.SEC_WEBSOCKET_PROTOCOL);
        return headers;
    }
}
